package sii.challenge.predictors;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import sii.challenge.dataset.Dataset;

/**
 * 
 * Predittore basato sull'approccio UserBasedNearestNeighborCollaborativeFiltering.
 * Maggiori informzioni nel materiale allegato al progetto.
 * 
 * @author Marco Liceti
 *
 */
public final class UserBasedPredictor implements Predictor {
	
	private static final int alpha = 20;
	private static final double beta = 0.5;
	
	private Dataset dataset;
	
	private static UserBasedPredictor instance;
	
	private UserBasedPredictor(Dataset dataset) {
		this.dataset = dataset;
	}
	
	/**
	 * 
	 * Restituisce l'istanza di UserBasedPredictor.
	 * 
	 * @param dataset L'istanza di Dataset necessaria al predittore
	 * 
	 * @return restituisce lo UserBasedPredictor
	 * 
	 */
	public static UserBasedPredictor getInstance(Dataset dataset) {
		if (instance == null) {
			instance = new UserBasedPredictor(dataset);
		}
		return instance;
	}

	@Override
	public int predictRating(int user, int item) {
		List<Integer> who_rated_item = dataset.getRatersOfItem(item);
		
		Map<Integer, List<Integer>> corated_map = new HashMap<Integer, List<Integer>>();
		for (Integer other_user : who_rated_item) {
			List<Integer> corated = dataset.getItemsCoRated(user, other_user);
			if (corated.size() >= alpha) {
				corated_map.put(other_user, dataset.getItemsCoRated(user, other_user));
			}
		}
		
		List<Integer> users_to_compare_with = new LinkedList<Integer>(corated_map.keySet());
		if (users_to_compare_with.size() < alpha) return -1;
		
		Map<Integer, Double> similarity_map = new HashMap<Integer, Double>();
		for (Integer other_user : users_to_compare_with) {
			List<Integer> corated = corated_map.get(other_user);
			double similarity = similarity(user, other_user, corated);
			if (Math.abs(similarity) >= beta) {
				similarity_map.put(other_user, similarity);
			}
		}
		
		List<Integer> nearest_neighbors = new LinkedList<Integer>(similarity_map.keySet());
		if (nearest_neighbors.size() < alpha) return -1;
		
		double average = getAverage(user, item);
		double predicted_delta = predictDelta(user, item, nearest_neighbors, similarity_map);

		return Utils.toRating(average + predicted_delta);
	}

	private double similarity(int user, int other_user, List<Integer> corated) {
		double n = 0;
		double d1 = 0;
		double d2 = 0;
		
		for (Integer item : corated) {
			double user_average = dataset.getRatingsGivenAverage(user);
			double other_user_average = dataset.getRatingsGivenAverage(other_user);
			
			int user_rating = dataset.getRating(user, item);
			int other_user_rating = dataset.getRating(other_user, item);
			
			double delta_user = user_rating - user_average;
			double delta_other_user = other_user_rating - other_user_average;
			
			n += delta_user * delta_other_user;
			d1 += delta_user * delta_user;
			d2 += delta_other_user * delta_other_user;
		}
		
		return n / (Math.sqrt(d1) * Math.sqrt(d2));
	}
	
	private double getAverage(int user, int item) {
		double average = 0;
		String item_gender = dataset.getItemGender(item);
		if (dataset.getNumberOfRatingsFromUser(user, item_gender) >= alpha) {
			average = dataset.getRatingsGivenAverage(user, item_gender);
		} else {
			average = dataset.getRatingsGivenAverage(user);
		}
		return average;
	}
	
	private double predictDelta(int user, int item,
			List<Integer> nearest_neighbors, Map<Integer, Double> similarity_map) {
		double n = 0;
		double d = 0;
		String item_gender = dataset.getItemGender(item);
		for (Integer other_user : nearest_neighbors) {
			double average = 0;
			if (dataset.getNumberOfRatingsFromUser(other_user, item_gender) >= alpha) {
				average = dataset.getRatingsGivenAverage(other_user, item_gender);
			} else {
				average = dataset.getRatingsGivenAverage(other_user);
			}
			double delta = dataset.getRating(other_user, item) - average;
			double similarity = similarity_map.get(other_user);
			n += similarity * delta;
			d += similarity;
		}
		return n / d;
	}

}
